package squirrel

import org.json4s._

import scala.util.{Success, Try}

object JSONWhere {

  private implicit val formats: Formats = DefaultFormats

  def parse(where: Option[JObject]): Try[SqlWhere] = where match {
    case None    => Success(SqlWhereEmpty)
    case Some(w) => Try(parseAnd(w))
  }

  def parseWhere(value: JValue): SqlWhere = value match {
    case v: JObject => parseAnd(v)
    case v: JArray  => parseAnd(v)
    case _          => throw new Exception("invalid where format")
  }

  def parseAnd(where: JObject): SqlWhere =
    SqlWhereAnd(where.extract[Map[String, JValue]].foldLeft(Seq[SqlWhere]()) { (sum, kv) =>
      sum :+ parseKV(kv)
    })

  def parseAnd(where: JArray): SqlWhere =
    SqlWhereAnd(where.fold(Seq[SqlWhere]()) { (sum, v) =>
      sum :+ parseWhere(v)
    })

  def parseAnd(where: JValue): SqlWhere = where match {
    case v: JObject => parseAnd(v)
    case v: JArray  => parseAnd(v)
    case _          => throw new Exception("invalid where format")
  }

  def parseOr(where: JObject): SqlWhere =
    SqlWhereOr(where.extract[Map[String, JValue]].foldLeft(Seq[SqlWhere]()) { (sum, kv) =>
      sum :+ parseKV(kv)
    })

  def parseOr(array: JArray): SqlWhere =
    SqlWhereOr(array.fold(Seq[SqlWhere]()) { (sum, v) =>
      sum :+ parseWhere(v)
    })

  def parseOr(value: JValue): SqlWhere = value match {
    case v: JObject => parseOr(v)
    case v: JArray  => parseOr(v)
    case _          => throw new Exception("invalid where format")
  }

  def parseNot(where: JObject): SqlWhere =
    SqlWhereNot(where.extract[Map[String, JValue]].foldLeft(Seq[SqlWhere]()) { (sum, kv) =>
      sum :+ parseKV(kv)
    })

  def parseNot(where: JArray): SqlWhere =
    SqlWhereNot(where.fold(Seq[SqlWhere]()) { (sum, v) =>
      sum :+ parseWhere(v)
    })

  def parseNot(value: JValue): SqlWhere = value match {
    case v: JObject => parseNot(v)
    case v: JArray  => parseNot(v)
    case _          => throw new Exception("invalid where format")
  }

  def parseKV(kv: (String, JValue)): SqlWhere =
    if (isOperator(kv._1)) {
      parseOpValue(kv._1, kv._2)
    } else {
      parseValueCond(kv._1, kv._2)
    }

  def parseOpValue(op: String, value: JValue): SqlWhere = op match {
    case "$and" => parseAnd(value)
    case "$or"  => parseOr(value)
    case "$not" => parseNot(value)
    case vop =>
      val SqlWhereCtor = SqlWhereCtorMap(vop)
      value match {
        case JObject(fields) =>
          val seq = fields.map {
            case (key, JNothing)    => SqlWhereCtor(key, SqlValueNull)
            case (key, JBool(v))    => SqlWhereCtor(key, v)
            case (key, JNull)       => SqlWhereCtor(key, SqlValueNull)
            case (key, JInt(v))     => SqlWhereCtor(key, v)
            case (key, JLong(v))    => SqlWhereCtor(key, v)
            case (key, JDecimal(v)) => SqlWhereCtor(key, v)
            case (key, JDouble(v))  => SqlWhereCtor(key, v)
            case (key, JString(v))  => SqlWhereCtor(key, v)
            case _                  => throw new Exception("invalid operand")
          }
          SqlWhereAnd(seq)
        case _ => throw new Exception("invalid where format")
      }
  }

  def parseValueCond(key: String, value: JValue): SqlWhere = value match {
    case JBool(v)    => SqlWhereEq(key, v)
    case JNull       => SqlWhereEq(key, SqlValueNull)
    case JString(v)  => SqlWhereEq(key, v)
    case JDouble(v)  => SqlWhereEq(key, v)
    case JDecimal(v) => SqlWhereEq(key, v)
    case JInt(v)     => SqlWhereEq(key, v)
    case JLong(v)    => SqlWhereEq(key, v)
    case v: JObject =>
      val conds = v.foldField(Seq[SqlWhere]()) { (sum, kv) =>
        kv._1 match {
          case "$and" => parseLogical(key, value)
          case "$or"  => parseLogical(key, value)
          case "$not" => parseLogical(key, value)
          case "$in" =>
            val values = kv._2 match {
              case JArray(lst) => lst.map(jValueToSqlValue)
            }
            Seq(SqlWhereIn(key, values.toSet))
          case _ =>
            if (isOperator(kv._1)) {
              val op   = kv._1
              val ctor = SqlWhereCtorMap(op)
              val cond = kv._2 match {
                case JNull       => ctor(key, SqlValueNull)
                case JString(s)  => ctor(key, s)
                case JDouble(s)  => ctor(key, s)
                case JDecimal(s) => ctor(key, s)
                case JInt(s)     => ctor(key, s)
                case JLong(s)    => ctor(key, s)
                case JBool(s)    => ctor(key, s)
                case _           => throw new Exception("unsupported value")
              }
              sum :+ cond
            } else {
              throw new Exception("must be op: value pairs")
            }
        }
      }
      SqlWhereAnd(conds)
    case v: JArray => ???
    case JNothing  => SqlWhereEq(key, SqlValueNull)
    case v: JSet   => ???
  }

  /**
    * parse logical value pair
    * @param fieldName field name
    * @param value pair of logical relation
    * @return
    */
  def parseLogical(fieldName: String, value: JValue): Seq[SqlWhere] = {
    value match {
      case JObject(pairs) =>
        pairs.map { kv =>
          kv._1 match {
            case "$and" => SqlWhereAnd(parseLogical(fieldName, kv._2))
            case "$or"  => SqlWhereOr(parseLogical(fieldName, kv._2))
            case "$not" => SqlWhereNot(parseLogical(fieldName, kv._2))
            case "$in" =>
              val values = kv._2 match {
                case JArray(lst) => lst.map(jValueToSqlValue)
              }
              SqlWhereIn(fieldName, values.toSet)
            case _ =>
              val ctor = SqlWhereCtorMap(kv._1)
              val cond = kv._2 match {
                case JNull       => ctor(fieldName, SqlValueNull)
                case JString(s)  => ctor(fieldName, s)
                case JDouble(s)  => ctor(fieldName, s)
                case JDecimal(s) => ctor(fieldName, s)
                case JInt(s)     => ctor(fieldName, s)
                case JLong(s)    => ctor(fieldName, s)
                case JBool(s)    => ctor(fieldName, s)
                case _ =>
                  throw new Exception(
                    s"unsupported value key: $fieldName, op = ${kv._1}, value = " +
                      s"${kv._2}")
              }
              cond
          }
        }
    }
  }

  def isOperator(str: String): Boolean = str.startsWith("$")

  private def jValueToSqlValue(value: JValue): SqlValue = value match {
    case JNull       => SqlValueNull
    case JString(s)  => SqlValueString(s)
    case JDouble(s)  => SqlValueNumber(s)
    case JDecimal(s) => SqlValueNumber(s)
    case JInt(s)     => SqlValueNumber(BigDecimal(s))
    case JLong(s)    => SqlValueNumber(s)
    case JBool(s)    => SqlValueBool(s)
    case _           => throw new Exception("unsupported value")
  }

  private val SqlWhereCtorMap = Map(
    "$eq"   -> SqlWhereEq,
    "$neq"  -> SqlWhereNotEq,
    "$gt"   -> SqlWhereGt,
    "$gte"  -> SqlWhereGte,
    "$lt"   -> SqlWhereLt,
    "$lte"  -> SqlWhereLte,
    "$like" -> SqlWhereLike,
  )
}
